// ===========================================================================
// CONTENT  : CLASS FileLocator
// AUTHOR   : Manfred Duchrow
// VERSION  : 1.3 - 14/03/2003
// HISTORY  :
//  17/05/2002  duma  CREATED
//	24/05/2002	duma	added		->	toURL(), isFile(), isDirectory(), getAbsolutePath()
//	21/06/2002	duma	added		->	realFile()
//	14/03/2003	duma	added		->	getStandardizedPath(), getStandardizedAbsolutePath()
//
// Copyright (c) 2002-2003, by Manfred Duchrow. All rights reserved.
// ===========================================================================
package org.pf.file;

// ===========================================================================
// IMPORTS
// ===========================================================================
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

import org.pf.text.StringUtil;

/**
 * This class mainly supports access to files which can be in the normal file directory structure or inside zip archives. The main purpose is to
 * provide methods that transparently treat files the same way whether they are in the normal directory structure or inside archives. The syntax is
 * simply to allow archive names in a path name at any place where a sub-directory name can be. <br>
 * Examples: <br>
 * <ul>
 * <li>d:\temp\archive.zip\config\nls.properties</li>
 * <li>/usr/java/jdk1.3/src.jar/java/io/File.java</li>
 * </ul>
 * 
 * @author Manfred Duchrow
 * @version 1.3
 */
public class FileLocator {
    // =========================================================================
    // CONSTANTS
    // =========================================================================
    private static final boolean DEBUG = false;
    private static final String FILE_PROTOCOL_INDICATOR = "file:" + File.separator;
    private static final String ARCHIVE_INDICATOR = "!" + File.separator;

    // =========================================================================
    // INSTANCE VARIABLES
    // =========================================================================
    private FileLocator parent = null;

    protected FileLocator getParent() {
        return parent;
    }

    protected void setParent(FileLocator newValue) {
        parent = newValue;
    }

    private File file = null;

    protected File getFile() {
        return file;
    }

    protected void setFile(File newValue) {
        file = newValue;
    }

    private ZipFile zipFile = null;

    protected ZipFile getZipFile() {
        return zipFile;
    }

    protected void setZipFile(ZipFile newValue) {
        zipFile = newValue;
    }

    private boolean exists = true;

    protected boolean getExists() {
        return exists;
    }

    protected void setExists(boolean newValue) {
        exists = newValue;
    }

    private Exception exception = null;

    protected Exception getException() {
        return exception;
    }

    protected void setException(Exception newValue) {
        exception = newValue;
    }

    // =========================================================================
    // CLASS METHODS
    // =========================================================================
    /**
     * Create a file locator that corresponds to the given file name.
     */
    public static FileLocator create(File file) {
        FileLocator locator = new FileLocator();

        return locator.createFrom(file);
    } // create()

    // -------------------------------------------------------------------------

    /**
     * Create a file locator that corresponds to the given file name.
     */
    public static FileLocator create(String filename) {
        return create(new File(filename));
    } // create()

    // -------------------------------------------------------------------------

    private static FileLocator newWith(FileLocator aParent, String[] pathElements) throws Exception {
        FileLocator locator = new FileLocator();

        return locator.createFrom(aParent, pathElements);
    } // newWith()

    // -------------------------------------------------------------------------

    // =========================================================================
    // CONSTRUCTORS
    // =========================================================================
    /**
     * Initialize the new instance with default values.
     */
    private FileLocator() {
        super();
    } // FileLocator()

    // -------------------------------------------------------------------------

    // =========================================================================
    // PUBLIC INSTANCE METHODS
    // =========================================================================

    /**
     * Returns the file that contains the data the locator points to. If the locator points to a normal file in a directory, than this file will be
     * returned. If the locator points to a file inside an archive, the file will be unzipped into the <i><b>temp</i></b> directory and this temp file
     * will be returned. If the locator points to a none existing file, this method returns false.
     */
    public File realFile() {
        File aFile;
        try {
            aFile = this.fileRef();
        } catch (Exception e) {
            aFile = null;
        }
        return aFile;
    } // realFile()

    // -------------------------------------------------------------------------

    /**
     * Returns whether or not the file specified by this locator exists.
     */
    public boolean exists() {
        return this.getExists();
    } // exists()

    // -------------------------------------------------------------------------

    /**
     * Returns whether or not the name specified by this locator points to a file.
     */
    public boolean isFile() {
        try {
            if (this.exists())
                return this.isFileElement(this.getFile());
            else
                return false;
        } catch (Exception e) {
            return false;
        }
    } // isFile()

    // -------------------------------------------------------------------------

    /**
     * Returns whether or not the name specified by this locator points to a directory.
     */
    public boolean isDirectory() {
        try {
            if (this.exists())
                return !this.isFileElement(this.getFile());
            else
                return false;
        } catch (Exception e) {
            return false;
        }
    } // isDirectory()

    // -------------------------------------------------------------------------

    /**
     * Returns the size of the file or 0 if it does not exist.
     */
    public long size() {
        ZipEntry entry;

        try {
            if (this.isInArchive()) {
                entry = this.archiveEntry();
                // if ( DEBUG ) com.mdcs.joi.Inspector.inspectWait( entry ) ;
                return entry.getSize();
            } else {
                return this.getFile().length();
            }
        } catch (Exception ex) {
            if (DEBUG)
                ex.printStackTrace();
            return 0L;
        }
    } // size()

    // -------------------------------------------------------------------------

    /**
     * Returns the timestamp of when the file was last modified or 0 in any case of error.
     */
    public long lastModified() {
        ZipEntry entry;

        try {
            if (this.isInArchive()) {
                entry = this.archiveEntry();
                return entry.getTime();
            } else {
                return this.getFile().lastModified();
            }
        } catch (Exception ex) {
            if (DEBUG)
                ex.printStackTrace();
            return 0L;
        }
    } // lastModified()

    // -------------------------------------------------------------------------

    /**
     * Returns an opened input stream on the file defined by this locator.
     */
    public InputStream getInputStream() throws Exception {
        ZipEntry entry;

        if (this.isInArchive()) {
            entry = this.archiveEntry();
            return this.container().getInputStream(entry);
        } else {
            return new FileInputStream(this.getFile());
        }
    } // getInputStream()

    // -------------------------------------------------------------------------

    /**
     * Returns whether or not the file specified by this locator is inside an archive.
     */
    public boolean isInArchive() {
        return this.getParent() != null;
    } // isInArchive()

    // -------------------------------------------------------------------------

    /**
     * Returns the full pathname.
     */
    public String getPath() {
        return this.fullFilePath(false).getPath();
    } // getPath()

    // -------------------------------------------------------------------------

    /**
     * Returns the full absolute pathname.
     */
    public String getAbsolutePath() {
        return this.fullFilePath(true).getPath();
    } // getAbsolutePath()

    // -------------------------------------------------------------------------

    /**
     * Returns the full pathname in a standardized for. That is all ".." and "." elements are removed and forward slashes are used as separators of
     * the remaining elements.
     */
    public String getStandardizedPath() {
        return this.fileUtil().standardize(this.getPath());
    } // getStandardizedPath()

    // -------------------------------------------------------------------------

    /**
     * Returns the full absolute pathname in a standardized form. That is all ".." and "." elements are removed and forward slashes are used as
     * separators of the remaining elements.
     */
    public String getStandardizedAbsolutePath() {
        return this.fileUtil().standardize(this.getAbsolutePath());
    } // getStandardizedAbsolutePath()

    // -------------------------------------------------------------------------

    /**
     * Returns the last exception that occured while using this locator or null, if no exception was thrown at all.
     */
    public Exception exception() {
        return this.getException();
    } // exception()

    // -------------------------------------------------------------------------

    /**
     * Returns the name of the file as an URL.
     */
    public URL toURL() throws MalformedURLException {
        StringBuffer buffer = new StringBuffer(128);

        this.urlPath(buffer);
        return new URL(buffer.toString());
    } // toURL()

    // -------------------------------------------------------------------------

    // =========================================================================
    // PROTECTED INSTANCE METHODS
    // =========================================================================

    protected FileLocator createFrom(File filePath) {
        FileLocator locator = null;
        String[] parts = null;
        File path = filePath;

        if (path.getPath().startsWith(FILE_PROTOCOL_INDICATOR))
            path = this.convertFromURLSyntax(path);

        parts = str().parts(path.getPath(), File.separator);
        try {
            locator = this.initFromPath(parts, path.getPath().startsWith(File.separator));
        } catch (Exception ex) {
            this.setException(ex);
            this.doesNotExist(path);
            locator = this;
        }
        return locator;
    } // createFrom()

    // -------------------------------------------------------------------------

    private FileLocator createFrom(FileLocator aParent, String[] pathElements) throws Exception {
        this.setParent(aParent);
        return this.initFromPath(pathElements, false);
    } // createFrom()

    // -------------------------------------------------------------------------

    protected FileLocator initFromPath(String[] parts, boolean startsFromRoot) throws Exception {
        FileLocator locator = this;
        File pathElement = null;
        String[] rest = null;
        boolean elementExists = false;

        if (startsFromRoot)
            pathElement = new File(File.separator);

        for (int i = 0; i < parts.length; i++) {
            if (pathElement == null)
                pathElement = new File(parts[i]);
            else
                pathElement = new File(pathElement, parts[i]);

            elementExists = this.doesElementExist(pathElement);

            if (elementExists) {
                this.setFile(pathElement);
                if (this.isFileElement(pathElement)) {
                    if (DEBUG)
                        System.out.println("Locator(" + pathElement + ")");
                    if (i < (parts.length - 1)) // Is not last element ?
                    {
                        rest = str().copyFrom(parts, i + 1);
                        // if (DEBUG) com.mdcs.joi.Inspector.inspect( "SubLocator", rest ) ;
                        locator = FileLocator.newWith(this, rest);
                    }
                    break;
                }
            } else {
                if (this.isInArchive()) {
                    if (i < (parts.length - 1)) // Is not last element ?
                    {
                        // Directories are not always identifiable individually in zip archives.
                        // Therefore it must be accepted that they are not found.
                        // So in such case no exception will be thrown.
                    } else {
                        throw new Exception("\"" + pathElement.getPath() + "\" does not exist");
                    }
                } else {
                    throw new Exception("\"" + pathElement.getPath() + "\" does not exist");
                }
            }
        }
        return locator;
    } // initFromPath()

    // -------------------------------------------------------------------------

    protected boolean doesElementExist(File element) throws Exception {
        if (this.isInArchive()) {
            return doesElementExistInArchive(element.getPath());
        } else {
            return element.exists();
        }
    } // doesElementExist()

    // -------------------------------------------------------------------------

    protected boolean isFileElement(File element) throws Exception {
        if (this.isInArchive()) {
            return isFileInArchive(element.getPath());
        } else {
            return element.isFile();
        }
    } // isFileElement()

    // -------------------------------------------------------------------------

    protected boolean doesElementExistInArchive(String elementName) throws Exception {
        ZipEntry entry;

        entry = this.entryFromArchive(elementName);

        return (entry != null);
    } // doesElementExistInArchive()

    // -------------------------------------------------------------------------

    protected boolean isFileInArchive(String elementName) throws Exception {
        ZipEntry entry;
        entry = this.entryFromArchive(elementName);

        // Unfortunately entry.isDirectory() returns false even for
        // pure directory entries inside a zip archive, so it can't be used here.
        // The trick below is problematic, because apart from
        // directories it will also not recognize files with size 0.

        return (entry != null) && (entry.getSize() > 0);
    } // isFileInArchive()

    // -------------------------------------------------------------------------

    protected ZipEntry entryFromArchive(String elementName) throws Exception {
        ZipEntry entry;
        ZipFile archive;
        String name;

        name = str().replaceAll(elementName, "\\", "/");
        archive = this.container();
        entry = archive.getEntry(name);

        if (DEBUG) {
            // if ( entry == null ) com.mdcs.joi.Inspector.inspect( name ) ;
            System.out.print(archive.getName() + "::" + name + " --- " + (entry != null));
            if (entry == null) {
                System.out.println();
            } else {
                System.out.print(" (" + entry.getSize() + ")");
                System.out.print(" (T:" + entry.getTime() + ")");
                System.out.println(" (" + (entry.isDirectory() ? "Dir" : "File") + ")");
            }
        }

        return entry;
    } // entryFromArchive()

    // -------------------------------------------------------------------------

    protected ZipEntry archiveEntry() throws Exception {
        return this.entryFromArchive(this.getFile().getPath());
    } // archiveEntry()

    // -------------------------------------------------------------------------

    protected void doesNotExist(File file) {
        this.setExists(false);
        this.setFile(file);
    } // doesNoTExist()

    // -------------------------------------------------------------------------

    protected File fullFilePath(boolean absolute) {
        File full;

        if (this.isInArchive()) {
            full = new File(this.getParent().fullFilePath(absolute), this.getFile().getPath());
        } else {
            if (absolute)
                full = this.getFile().getAbsoluteFile();
            else
                full = this.getFile();
        }

        return full;
    } // fullFilePath()

    // -------------------------------------------------------------------------

    protected void urlPath(StringBuffer buffer) {
        if (this.isInArchive()) {
            this.getParent().urlPath(buffer);
            buffer.append(ARCHIVE_INDICATOR);
        } else {
            buffer.append(FILE_PROTOCOL_INDICATOR);
        }
        buffer.append(this.getFile().getPath());
    } // urlPath()

    // -------------------------------------------------------------------------

    protected File fileRef() throws Exception {
        InputStream archiveStream;
        FileOutputStream fileStream;
        ZipEntry entry;
        File tempFile;

        if (this.isInArchive()) {
            entry = this.archiveEntry();
            archiveStream = this.container().getInputStream(entry);
            tempFile = File.createTempFile("FLOC_", ".xtr");
            tempFile.deleteOnExit();
            fileStream = new FileOutputStream(tempFile);
            fileUtil().copyStream(archiveStream, fileStream);
            return tempFile;
        } else {
            return this.getFile();
        }
    } // fileRef()

    // -------------------------------------------------------------------------

    /**
     * Returns the file this locator presents as opened zip file or null in any case of error.
     */
    protected ZipFile archive() throws Exception {
        if (this.getZipFile() == null) {
            this.setZipFile(new ZipFile(this.fileRef()));
        }
        return this.getZipFile();
    } // archive()

    // -------------------------------------------------------------------------

    /**
     * Returns the zip file which is presented by the parent container or null in any case of error.
     */
    protected ZipFile container() throws Exception {
        if (this.isInArchive())
            return this.getParent().archive();
        else
            return null;
    } // container()

    // -------------------------------------------------------------------------

    protected File convertFromURLSyntax(File file) {
        String newStr;

        newStr = file.getPath().substring(FILE_PROTOCOL_INDICATOR.length());
        newStr = str().replaceAll(newStr, ARCHIVE_INDICATOR, File.separator);

        return new File(newStr);
    } // convertFromURLSyntax()

    // -------------------------------------------------------------------------

    protected StringUtil str() {
        return StringUtil.current();
    } // str()

    // -------------------------------------------------------------------------

    protected FileUtil fileUtil() {
        return FileUtil.current();
    } // fileUtil()

    // -------------------------------------------------------------------------

} // class FileLocator